package org.dfl.plugin.runner;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.dfl.core.model.TransitionFireResult;
import org.dfl.core.model.transitions.Transition;
import org.dfl.core.utils.Utils;
import org.dfl.messages.Messages;
import org.dfl.plugin.dialogs.DialogsManager;
import org.dfl.plugin.editor.model.DflDiagram;
import org.dfl.plugin.editor.model.TransitionModelElement;
import org.eclipse.swt.SWT;

/**
 * a class that runs the flow 
 * 
 * @author Piotr Wlodarczyk
 */
public class FlowRunner extends Thread {
	private static Logger logger = Logger.getLogger(FlowRunner.class);
	
	// listeners to fire run changes like pause/resume
	private Set<RunListener> listeners =  new HashSet<RunListener>();
	
	// diagram that is being run
	private DflDiagram diagram;
	
	// is paused
	private boolean paused = false;
	
	// should be paused after next step
	private boolean stepMode = false;
	
	// should disable result tokens - this works only with stepMode = true
	private boolean disableOnExit = false;
	
	// is running
	private boolean running = false;
	private boolean runningTransition = false;

	private HashSet<TransitionModelElement> transitionsToRun;
	
	@SuppressWarnings("unchecked") //$NON-NLS-1$
	public FlowRunner(DflDiagram diagram, Set listeners) {
		super();
		this.diagram = diagram;
		this.listeners.addAll(listeners);
		transitionsToRun = new HashSet<TransitionModelElement>(diagram.getTransitions());
	}

	/**
	 * run the dataflow - transitions check their inputs in a loop until stop action is performed
	 */
	@Override
	public void run() {
		// first run all transitions that has no inputs - should run them only
		// once!
		Set<TransitionModelElement> transitionsWithNoInputs = getTransitionsWithNoInputs(transitionsToRun);
		for (TransitionModelElement t : transitionsWithNoInputs) {
			if(!checkRunning()) return;
			tryToRun(t);
			transitionsToRun.remove(t);
		}

		boolean executed = false;
		boolean isStepMode = false;
		do {
			if(!checkRunning()) return;
			isStepMode = isStepMode();
			executed = false;
			
			// get transition allowed to run
			List<TransitionModelElement> transitionReadyToRun = new ArrayList<TransitionModelElement>();
			for (TransitionModelElement t : transitionsToRun) {
				if (t.getTransition().isReadyToRun()) {
					transitionReadyToRun.add(t);
					if(!isStepMode) break;
				}
			}

			if (transitionReadyToRun.isEmpty()) {
				// should not happen as long as step mode button is disabled
				if(stepMode) DialogsManager.openErrorDialog(Messages.FlowRunner_1);
			} else if (transitionReadyToRun.size() == 1 || !isStepMode) {
				executed = (tryToRun(transitionReadyToRun.get(0)) || executed);
			} else if(isStepMode){
				final Map<String, TransitionModelElement> transitionLabelsMap = new HashMap<String, TransitionModelElement>();
				final Collection<String> col = new ArrayList<String>();
				for (TransitionModelElement t : transitionReadyToRun) {
					String label = t.getTransition().getName()
							+ " (" + t.getTransition().getId() + ")"; //$NON-NLS-1$ //$NON-NLS-2$
					col.add(label);
					transitionLabelsMap.put(label, t);
				}

				final ArrayList<TransitionModelElement> tt = new ArrayList<TransitionModelElement>();
				Utils.execSynch(new Runnable() {
					public void run() {
						String res = (String) DialogsManager.openSelectDialog(Messages.FlowRunner_4, Messages.FlowRunner_4, col);
						if (res!=null) {
							tt.add(transitionLabelsMap.get(res));
						}
					}
				});

				if (!tt.isEmpty()) {
					tryToRun(tt.get(0));
				}
			}
			
			if(isStepMode()){
				setStepMode(false, false);
				setPaused(true);
			}

			if(!executed){
				try {
					Thread.sleep(200);
				} catch (InterruptedException e) {}
			}
		} while (isRunning());
		finish();
	}
	
	/**
	 * check if there is any transition ready to run
	 * @return true/false
	 */
	private Transition lastFoundReadyToRunTransiton = null;
	public boolean isReadyToRun() {
		if(lastFoundReadyToRunTransiton!=null && lastFoundReadyToRunTransiton.isReadyToRun()){
			return true;
		}
		for (TransitionModelElement t : transitionsToRun) {
			if(t.getTransition().isReadyToRun()){
				lastFoundReadyToRunTransiton = t.getTransition();
				return true;
			}
		}
		return false;
	}
	
	private boolean tryToRun(TransitionModelElement tme) {
		Transition t = tme.getTransition();
		try {
			TransitionFireResult res = t.initRun();
			// run
			if (res.exception != null) {
				showErrorAndPause(res.exception);
				return false;
			} else if (res.isReadyToRun) {
				setRunningTransition(true);
				tme.runningStarted();
				
				run(t,res);

				if (res.exception != null) {
					showErrorAndPause(res.exception);
				} else if (checkRunning()) {
					setRunningTransition(false);
					t.finishRun(res, (isStepMode() && disableOnExit));
					tme.runningFinished();
					return true;
				}

				tme.runningFinished();
				return false;
			} else {
				return false;
			}
		} catch (Throwable e) {
			showErrorAndPause(e);
			return false;
		}
	}
	
	Thread th = null;
	private void run(final Transition t, final TransitionFireResult res) {
		th = new Thread(this){
			@Override
			public void run() {
				t.run(res);
			}
		};
		th.start();
		int j=4;
		int minutes = 0;
		do{
			j++;
			int i = 600*j;
			minutes += j;
			while(i>0 && th.isAlive() && isRunning() && res.outputData==null){
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
				}
				i--;
			}
			if(th.isAlive() && isRunning() && res.outputData==null){
				int qRes = DialogsManager.openDialog("Terminate/Restart/Continue?", "Transition \""+t.getName()+"\" invokes over "+minutes+" minutes.\n To restart press 'Retry'? \n To terminate press 'Abort'.", (SWT.ICON_WORKING |SWT.ABORT | SWT.RETRY | SWT.IGNORE));
				if(qRes == SWT.ABORT && th.isAlive() && isRunning() && res.outputData==null){
					stopTransitionExecution();
					logger.info(t.getName()+" - execution aborted");
					setPaused(true);
					return; // aborted
				}else if(qRes == SWT.RETRY && th.isAlive() && isRunning() && res.outputData==null){
					stopTransitionExecution();
					logger.info(t.getName()+" - execution restarted");
					run(t, res);
					return;
				}
			}
		}while(th.isAlive() && isRunning() && res.outputData==null);
		stopTransitionExecution();
	}
	
	@SuppressWarnings("deprecation")
	public void stopTransitionExecution(){
		if(th!=null && th.isAlive()){
			try{th.interrupt();}catch(Throwable e){}
			try{th.stop();}catch(Throwable e){}
		}
	}

	private void showErrorAndPause(Throwable e){
		logger.error(e.getMessage(),e);
		if(!DialogsManager.openOkCancelDialog("Error", e.getMessage()+"\n"+"To continue press YES, to pause press NO.")){
			setPaused(true);
		}
	}

	private boolean checkRunning() {
		while(isPaused() && isRunning()){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
			}
		}
		return isRunning();
	}

	private Set<TransitionModelElement> getTransitionsWithNoInputs(
			Set<TransitionModelElement> transitions) {
		Set<TransitionModelElement> res = new HashSet<TransitionModelElement>();
		for (TransitionModelElement t : transitions) {
			if (t.getTransition().inputsCount() == 0
					&& t.getTransition().areAllInputsDefined()) {
				res.add(t);
			}
		}
		return res;
	}

	@SuppressWarnings("deprecation") //$NON-NLS-1$
	public void finish() {
		setRunning(false);
	}

	public boolean isPaused() {
		return paused;
	}

	public void setPaused(boolean paused) {
		this.paused = paused;
		ArrayList<RunListener> ll = new ArrayList<RunListener>(listeners);
		if (paused) {
			for (RunListener l : ll)
				l.diagramPaused(diagram);
		} else {
			for (RunListener l : ll)
				l.diagramResumed(diagram);
		}
	}

	public boolean isRunningTransition() {
		return runningTransition;
	}

	public void setRunningTransition(boolean runningTransition) {
		this.runningTransition = runningTransition;
	}

	public boolean isRunning() {
		return running;
	}

	public void setRunning(boolean running) {
		this.running = running;
		ArrayList<RunListener> ll = new ArrayList<RunListener>(listeners);
		if (running) {
			for (RunListener l : ll)
				l.diagramRun(diagram);
		} else {
			for (RunListener l : ll)
				l.diagramStopped(diagram);
		}
	}

	public boolean isStepMode() {
		return stepMode;
	}
	
	public void setStepMode(boolean stepMode, boolean disableOnExit) {
		this.stepMode = stepMode;
		if(stepMode) {
			setPaused(false);
			this.disableOnExit = disableOnExit;
		}
	}
	
	public void addListener(RunListener listener) {
		listeners.add(listener);
	}
	
	public void removeListener(RunListener listener) {
		listeners.remove(listener);
	}
	
}
